/* Copyright (c) 2007  Peter Palotas
 *  
 *  This software is provided 'as-is', without any express or implied
 *  warranty. In no event will the authors be held liable for any damages
 *  arising from the use of this software.
 *  
 *  Permission is granted to anyone to use this software for any purpose,
 *  including commercial applications, and to alter it and redistribute it
 *  freely, subject to the following restrictions:
 *  
 *      1. The origin of this software must not be misrepresented; you must not
 *      claim that you wrote the original software. If you use this software
 *      in a product, an acknowledgment in the product documentation would be
 *      appreciated but is not required.
 *  
 *      2. Altered source versions must be plainly marked as such, and must not be
 *      misrepresented as being the original software.
 *  
 *      3. This notice may not be removed or altered from any source
 *      distribution.
 *  
 *  
 *  $Id: AsyncStreamReader.cs 19 2007-08-15 13:14:32Z palotas $
 */

using System;
using System.Text;
using System.IO;
using System.Threading;
using System.Globalization;
using C5;
using Plossum.Resources;

namespace Plossum.IO
{
    /// <summary>
    /// A <see cref="System.IO.TextReader"/> providing line based asynchronous reading for a stream.
    /// </summary>
    public class AsyncStreamReader : TextReader 
    {
        #region Public properties

        /// <summary>
        /// Returns the underlying stream. 
        /// </summary>
        /// <value>The underlying stream.</value>
        public Stream BaseStream
        {
            get
            {
                return mStream;
            }
        }

        /// <summary>
        /// Gets the current character encoding that the current AsyncStreamReader object is using
        /// </summary>
        /// <value>The current character encoding used by the current reader.</value>
        public Encoding CurrentEncoding
        {
            get
            {
                return mEncoding;
            }
        }

        #endregion
        
        #region Public constructors and destructor

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="AsyncStreamReader"/> is reclaimed by garbage collection.
        /// </summary>
        ~AsyncStreamReader()
        {
            Dispose(false);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="AsyncStreamReader"/> class with a default encoding and buffer size.
        /// </summary>
        /// <param name="stream">The stream to wrap.</param>
        /// <exception cref="ArgumentException">If the stream does not support reading, 
        /// or if <paramref name="bufferSize"/> is not a positive number</exception>
        public AsyncStreamReader(Stream stream)
            : this(stream, 1024)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="AsyncStreamReader"/> class with a default buffer size.
        /// </summary>
        /// <param name="stream">The stream to wrap.</param>
        /// <param name="encoding">The encoding to use.</param>
        /// <exception cref="ArgumentException">If the stream does not support reading, 
        /// or if <paramref name="bufferSize"/> is not a positive number</exception>
        public AsyncStreamReader(Stream stream, Encoding encoding)
            : this(stream, encoding, 1024)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="AsyncStreamReader"/> class with a default encoding.
        /// </summary>
        /// <param name="stream">The stream to wrap.</param>
        /// <param name="bufferSize">Size of the buffer to use. This value must be greater than zero.</param>
        /// <exception cref="ArgumentException">If the stream does not support reading, 
        /// or if <paramref name="bufferSize"/> is not a positive number</exception>
        public AsyncStreamReader(Stream stream, int bufferSize)
            : this(stream, Encoding.UTF8, bufferSize)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="AsyncStreamReader"/> class.
        /// </summary>
        /// <param name="stream">The stream to wrap.</param>
        /// <param name="bufferSize">Size of the buffer to use. This value must be greater than zero.</param>
        /// <param name="encoding">The encoding to use.</param>
        /// <exception cref="ArgumentException">If the stream does not support reading, 
        /// or if <paramref name="bufferSize"/> is not a positive number</exception>
        public AsyncStreamReader(Stream stream, Encoding encoding, int bufferSize)
        {
            if (stream == null)
                throw new ArgumentNullException("stream");

            if (encoding == null)
                throw new ArgumentNullException("encoding");

            if (!stream.CanRead)
                throw new ArgumentException("Stream does not support reading");

            if (bufferSize <= 0)
                throw new ArgumentException("Buffer size must be greater than 0");

            mStream = stream;
            mBuffer = new byte[bufferSize];
            mCharBuffer = new CircularQueue<char>(bufferSize);
            
            mEncoding = encoding;
            mDecoder = encoding.GetDecoder();

            mAsyncResult = mStream.BeginRead(mBuffer, 0, mBuffer.Length, AsyncCallback, null);
        }
        #endregion

        #region Public methods
        /// <summary>
        /// Begins the asynchronous read operation on the stream. This means that for each line
        /// that becomes available on the input stream the delegate DataReceived will be called.
        /// After this call has been made, and before CancelReadLine is called no other 
        /// synchronous read operations may be performed on the stream.
        /// </summary>
        /// <exception cref="IOException">Attempted an asynchronous read past the end of the stream, or a disk error occurs.</exception>
        /// <exception cref="ObjectDisposedException">Method was called after the stream was closed.</exception>
        /// <exception cref="InvalidOperationException">Method was called when a synchronous read operation was in progress</exception>
        public void BeginReadLine()
        {
            if (!Monitor.TryEnter(mSyncReadLock))
                throw new InvalidOperationException(IOStrings.CannotStartAsynchronousReadWhenSynchronousReadOperationIsInProgress);

            lock (mLock)
            {
                mDoAsync = true;
                mNeedData.Set();
            }

            Monitor.Exit(mSyncReadLock);
        }

        /// <summary>
        /// Cancels the asynchronous read operations. After this call synchronous read operations may be performed again.
        /// </summary>
        public void CancelReadLine()
        {
            lock (mLock)
            {
                mDoAsync = false;
            }
        }

        /// <summary>
        /// Reads the next character from the input stream and advances the character position by one character.
        /// </summary>
        /// <returns>
        /// The next character from the input stream, or -1 if no more characters are available. 
        /// </returns>
        /// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
        /// <exception cref="InvalidOperationException">Method was called when an asynchronous read operation was in 
        /// progress, i.e. BeginReadLine() has been called without a matching call to CancelReadLine()</exception>
        /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:System.IO.TextReader"></see> is closed. </exception>
        public override int Read()
        {
            lock (mSyncReadLock)
            {
                if (mDoAsync)
                    throw new InvalidOperationException("cannot perform synchronous operations on AsyncStreamReader when asynchronous operation is in progress");

                if (mEOS)
                    return -1;

                mNeedData.Set();
                mHasData.WaitOne();

                lock (mLock)
                {
                    if (mCharBuffer.IsEmpty)
                    {
                        // Now end of stream was reached without any new
                        // characters read.
                        return -1;
                    }
                    else
                    {
                        char ch = mCharBuffer.Dequeue();
                        if (mCharBuffer.IsEmpty)
                            mHasData.Reset();
                        return ch;
                    }
                }
            }
        }

        /// <summary>
        /// Reads the next character without changing the state of the reader or the character source. 
        /// Returns the next available character without actually reading it from the input stream.
        /// </summary>
        /// <returns>
        /// The next character to be read, or -1 if no more characters are available.
        /// </returns>
        /// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
        /// <exception cref="InvalidOperationException">Method was called when an asynchronous read operation was in 
        /// progress, i.e. BeginReadLine() has been called without a matching call to CancelReadLine()</exception>
        /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:System.IO.TextReader"></see> is closed. </exception>
        public override int Peek()
        {
            lock (mSyncReadLock)
            {
                if (mDoAsync)
                    throw new InvalidOperationException("cannot perform synchronous operations on AsyncStreamReader when asynchronous operation is in progress");

                if (mEOS)
                    return -1;

                mNeedData.Set();
                mHasData.WaitOne();

                lock (mLock)
                {
                    if (mCharBuffer.IsEmpty)
                    {
                        return -1;
                    }
                    else
                    {
                        return mCharBuffer[0];
                    }
                }
            }
        }

        /// <summary>
        /// Closes the <see cref="T:System.IO.TextReader"></see> and releases any system resources associated 
        /// with the TextReader.
        /// </summary>
        public override void Close()
        {
            try
            {
                mStream.Close();
            }
            finally
            {
                base.Close();
            }
        }

        #endregion            

        #region Public events
        /// <summary>
        /// Occurs when someone writes to the stream if BeginReadLine() has been called.
        /// </summary>
        public event EventHandler<AsyncDataReceivedEventArgs> DataReceived;

        #endregion

        #region Private methods

        /// <summary>
        /// Performs the callback to DataReceived if an entire line has been read, or buffers the contents otherwise.
        /// Multiple calls to DataReceived will be made if several lines of data has been read.
        /// </summary>
        private void PerformCallback()
        {
            while (ReadLineFromBuffer(mStringBuffer))
            {
                if (DataReceived != null)
                    DataReceived(this, new AsyncDataReceivedEventArgs(mStringBuffer.ToString()));
                mStringBuffer.Length = 0;
            }
        }

        /// <summary>
        /// The internal callback used by mStream.BeginRead().
        /// </summary>
        /// <param name="ar">The ar.</param>
        private void AsyncCallback(IAsyncResult ar)
        {
            try
            {
                Monitor.Enter(mLock);
                int size = mStream.EndRead(ar);

                if (size > 0)
                {
                    char[] charArray = new char[mDecoder.GetCharCount(mBuffer, 0, size)];
                    mDecoder.GetChars(mBuffer, 0, size, charArray, 0, false);

                    foreach (char c in charArray)
                        mCharBuffer.Enqueue(c);

                    if (mDoAsync)
                    {
                        mNeedData.Reset();
                        PerformCallback();
                        mAsyncResult = mStream.BeginRead(mBuffer, 0, mBuffer.Length, AsyncCallback, null);
                    }
                    else
                    {
                        mHasData.Set();
                        Monitor.Exit(mLock);
                        mNeedData.WaitOne();
                        Monitor.Enter(mLock);
                        mNeedData.Reset();
                        mAsyncResult = mStream.BeginRead(mBuffer, 0, mBuffer.Length, AsyncCallback, null);
                    }
                }
                else
                {
                    if (DataReceived != null && mDoAsync)
                        DataReceived(this, new AsyncDataReceivedEventArgs(null));

                    mHasData.Set();
                    mEOS = true;
                }
            }
            finally
            { 
                try
                {
                    Monitor.Exit(mLock);
                }
                catch (SynchronizationLockException)
                {
                }
            }
        }

        /// <summary>
        /// Attempts to read a complete line from the internal character buffer. It requires exclusive access,
        /// so mLock must be free upon calling this method. If a complete line was read, <c>true</c> is returned,
        /// otherwise <c>false</c> is returned, meaning the character buffer is now empty. In either case, the
        /// read characters will be added to <paramref name="builder"/> and removed from the character buffer.
        /// A line is defined as a series of characters ending with a carriage return ('\r'), a line feed ('\n')
        /// or a carriage return followed by a line feed ("\r\n"). The line ending character is not added to the
        /// <paramref name="builder"/>.
        /// </summary>
        /// <param name="builder">The builder to which to append characters.</param>
        /// <returns><c>true</c> if a newline was read, <c>false</c> otherwise.</returns>
        private bool ReadLineFromBuffer(StringBuilder builder)
        {           
            lock (mLock)
            {
                while (!mCharBuffer.IsEmpty)
                {
                    char ch = mCharBuffer.Dequeue();
                    if (ch == '\r')
                    {
                        if (!mCharBuffer.IsEmpty && mCharBuffer[0] == '\n')
                            mCharBuffer.Dequeue();
                        return true;
                    }
                    else if (ch == '\n')
                    {
                        return true;
                    }
                    else
                    {
                        builder.Append(ch);
                    }
                }
                return false;
            }
        }

        /// <summary>
        /// Releases the unmanaged resources used by the <see cref="T:System.IO.TextReader"></see> and optionally 
        /// releases the managed resources.
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to release 
        /// only unmanaged resources.</param>
        protected override void Dispose(bool disposing)
        {
            try
            {
                mStream.Dispose();
                mHasData.Close();
                mNeedData.Close();
            }
            finally
            {
                base.Dispose(disposing);
            }
        }

        #endregion

        #region Private fields

        private CircularQueue<char> mCharBuffer;
        private byte[] mBuffer;
        private StringBuilder mStringBuffer = new StringBuilder();

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
        private IAsyncResult mAsyncResult;

        private Stream mStream;

        /// <summary>
        /// Lock object used by any synchronous method before setting mNeedData and attempting to wait on mHasData and not 
        /// releasing until operation completed.
        /// </summary>
        private object mSyncReadLock = new object();


        /// <summary>
        /// Lock object to prevent data corruption. Used by most methods during access to private fields.
        /// </summary>
        private object mLock = new Object();

        /// <summary>
        /// Indication if asynchronous operation is active or not. Set by BeginReadLine.
        /// </summary>
        private bool mDoAsync; // defaults to false


        private Encoding mEncoding;
        private Decoder mDecoder;

        /// <summary>
        /// Indicates if the underlying stream has reached its end
        /// </summary>
        private bool mEOS;

        /// <summary>
        /// WaitHandle set when data is needed by synchronous operation, and reset when ReadLine was called 
        /// for the underlying stream by AsyncCallback. Used by AsyncCallback to not read data when it is not
        /// needed.
        /// </summary>
        private EventWaitHandle mNeedData = new EventWaitHandle(false, EventResetMode.ManualReset);
        private EventWaitHandle mHasData = new EventWaitHandle(false, EventResetMode.ManualReset);
        #endregion 
    


    }
}
